/////////////////////////////////////////////////////////////////////
//  Package Description:
//    BBC iPlayer plugin for MythTV by Grant French (c) 2008-2009
//
//  Filename:  main.cpp
//  Author:    Grant French
//  Date:      06/12/2008
//
//  Description:
//    Download & Parses BBC iPlayer RSS/Atom feeds, then saves
//    to database tables.
//
/////////////////////////////////////////////////////////////////////

#include "xmlparser.h"
#include <mythtv/mythdbcon.h>
#include <mythtv/mythcontext.h>

/* Record Class */
Record::Record() {
}
Record::~Record() {
}
bool Record::saveToDB() {
  /* Saves the current record information to the database */
  intid=-1;

  /* Insert data into iplayer_metadata
      childid, browse, catid, userrating and showlevel are all static defaults */
  MSqlQuery query(MSqlQuery::InitCon());
  query.prepare("INSERT INTO iplayer_metadata "
                "(  title, description, series, rating, "
                "   filename, coverfile, inetref, url, "
                "   userrating, playcommand, showlevel) "
                " VALUES "
                "(  :TITLE, :DESC, :SERIES, :RATING, "
                "   :FILE, :COVER, :INETREF, :URL, "
                "   0.0, NULL, 1 )");

  query.bindValue(":TITLE", this->title);
  query.bindValue(":DESC", this->description);
  query.bindValue(":SERIES", this->series);
  query.bindValue(":RATING", this->rating);
  query.bindValue(":FILE", this->filename);
  query.bindValue(":COVER", this->coverfile);
  query.bindValue(":INETREF", this->inetref);
  query.bindValue(":URL", this->url);

  //If the query executes successfully
  if (query.exec() && query.isActive())
  {
    //Get the ID of the inserted record
    query.exec("SELECT LAST_INSERT_ID()");
    if (query.isActive() && query.size() > 0)
    {
      query.next();
      intid = query.value(0).toUInt();
    }

  } else {
    printf("DB Query error occurred %s",query.lastError().text().ascii());
    return false;
  }

  //If the main insert failed don't move on to inserting genres
  if (intid == -1) return FALSE;

  //If there are one or more genres iterate through them and link them to the new file
  if (genre.count() > 0) {

    //TODO: Have the genres list passed into the record object to save generating
    //        it for every record we process.
    //        (also this will break for more than 71 genres)

    //Get all genres in the DB and compile a dictionary of them name -> id
    QDict<QVariant> genres(71,false);
    query.prepare("SELECT intid,genre FROM iplayer_genre");
    query.exec();
    if (query.exec() && query.isActive() && query.size() > 0)
    {
      while( query.next() ) {
        genres.insert( query.value(1).toString(), new QVariant( query.value(0).toInt() ) );
      }
    }

    //TODO: Can this support multi row inserts?
    //Link this record against all genres specified in the genre string list
    QString strQuery = "INSERT INTO iplayer_metadatagenre (idprogramme,idgenre) VALUES (:IDPROGRAMME,:IDGENRE)";
    for (QStringList::const_iterator iter = genre.begin(); iter != genre.end();
         ++iter)
    {
       //Lookup genre in dictionary
      if ( ! genres[*iter] )
      {
        //If the requested genre is not present add it
        printf("Genre \"%s\" not found, i will add it now.\n",(*iter).ascii());

        //Insert new genre
        query.prepare("INSERT INTO iplayer_genre (genre) VALUES (:GENRE)");
        query.bindValue(":GENRE",(*iter));

        if ( query.exec() ) {

          //If the insert succeeded, get the new genre's id
          query.exec("SELECT LAST_INSERT_ID()");
          if (query.isActive() && query.size() > 0)
          {
            query.next();

            //Add the new genre to the genres dictionary
            genres.insert( *iter, new QVariant(query.value(0).toUInt()) );

          } else {
            return false;
          }

        } else {
          printf("DB Query error failed to insert new genre. (%s)\n",query.lastError().text().ascii());
          return false;
        }
      }

      //Get the genre id from it's name (using the dictionary)
      int tmp = genres[*iter]->toInt();

      //Insert the link between this record and the genre
      query.prepare(strQuery);
      query.bindValue(":IDPROGRAMME",QVariant(intid));
      query.bindValue(":IDGENRE",QVariant(tmp));
      query.exec();

    }

  }

  return true;
}



/* XMLHandler Class */
bool XMLHandler::startDocument() {

  //On the start of an XML document initialise the inEntry property
  inEntry=false;
  return TRUE;
}

bool XMLHandler::startElement( const QString&, const QString& localName, const QString&, const QXmlAttributes &attrs )
{

  //Push this node onto the stack (so we know where we are)
  stack.push(localName);

  //If we are not already in an entry and we have just encountered an entry node
  if (!inEntry && localName=="entry") {

    //Create a new programme record, and set the inEntry flag
    rec = new Record();
    inEntry=true;

    return TRUE;
  }

  //If in an entry node, process tags, else ignore them
  if (inEntry) {

    //Category tags hold the genres for a program
    if (localName == "category") {

      //The term attribute holds the genre name,
      //  if present add to the programme record.
      int idxCat = attrs.index( "term" );
      if ( idxCat != -1 ) {
        rec->genre.append( attrs.value( idxCat ) );
      }

    //BBC use the link nodes for holding the series name & programme title
    } else if (localName == "link") {

      //The rel attribute is either releated for the series, or
      //  self for the programme name, title holds the text description.
      int idxRel = attrs.index( "rel" );
      int idxTitle = attrs.index( "title" );

      if (idxRel != -1 && idxTitle != -1) {
        if (attrs.value( idxRel ) == "related") {
          rec->series = attrs.value( idxTitle );

        } else if (attrs.value( idxRel ) == "self") {
          rec->title = attrs.value( idxTitle );

        }
      }
    }
  }
  return TRUE;
}

bool XMLHandler::endElement( const QString&, const QString& localName, const QString& )
{
  //Remove the last element added to the stack to keep track of where we are
  stack.pop();

  //If we are in an entry and this is the end of that entry node
  if ( inEntry && localName == "entry" ) {

    //Commit programme record to DB (after some processing)

    //Remove HTML from the description & unneeded whitespace
    rec->description.replace( QRegExp("<[^>]+>")," ");
    rec->description.replace( QRegExp("(\\s)\\s+"),"\\1");

    //Create the URL to te cover file
    rec->coverfile = QString("http://www.bbc.co.uk/iplayer/images/episode/").append(rec->inetref).append("_640_360.jpg");

    //Create the pseudo filename for the programme (used to display folder structure in mythtv interface)
    rec->filename = QString("/iplayer/").append(QString(rec->series).replace("/","-")).append("/").append(QString(rec->title).replace("/","-"));

    //Create the URL to the actual programme (the flash player & programme id)
    rec->url = QString("http://www.bbc.co.uk/emp/9player.swf?revision=6928_7030&embedReferer=http://www.mythtv.org&embedPageUrl=http://www.bbc.co.uk/iplayer/episode/").append(rec->inetref).append("/").append(QString(rec->title).replace(" ","_")).append("/&domId=bip-play-emp&config=http://www.bbc.co.uk/emp/iplayer/config.xml&playlist=http://www.bbc.co.uk/iplayer/playlist/").append(rec->inetref).append("&config_settings_suppressCodec=h264&config_settings_autoPlay=true&guidance=unset");

    //User rating is initialised to 0.0
    rec->userrating = 0.0;

    //TODO: some programs are marked for guidance, would be nice to extract this, though it is not in the feed
    //Programme Rating e.g. parent guidance
    rec->rating = "";

    //Tell the record to commit itself.
    rec->saveToDB();

    // printf("Commit to DB\ntitle: %s\ndescription: %s\nseries: %s\nrating: %s\nuserrating: %f\nfilename: %s\ncoverfile: %s\ninetref: %s\nurl: %s\n", rec->title.ascii(), rec->description.ascii(), rec->series.ascii(), rec->rating.ascii(), rec->userrating, rec->filename.ascii(), rec->coverfile.ascii(), rec->inetref.ascii(), rec->url.ascii());

    //We are no longer in an entry node
    inEntry=false;
  }

  return TRUE;
}

bool XMLHandler::characters( const QString& ch ) {

  if ( inEntry ) { //If we are in an entry

    //printf("Top of stack is: %s \n\n",stack.top().ascii());

    //Use the stack to work out what node we are in, and store
    //  the data in the appropriate record field
    if ( stack.top() == "title" ) {
      rec->title = ch;

    } else if ( stack.top() == "content" ) {
      rec->description.append(ch);

    } else if ( stack.top() == "id" ) {
      //Most of the ID is garbage to us, we just want the last part
      rec->inetref=ch.section( ':', -1 );

    }
  }
  return TRUE;
}

/* XMLParser Class */
XMLParser::XMLParser(const QString &xml_url) : QObject()
{
  // initialise flags used for downloading data
  errorFlag = false;
  processingDownload = false;

  // create the download buffers etc. and attach to the requestFinished signal
  QUrl url( xml_url );
  http = new QHttp();
  buff = new QBuffer(bytes);
  QObject::connect( http, SIGNAL(requestFinished(int, bool)), this, SLOT(httpFinished(int, bool)));

  // exit if we can't open the buffer
  if ( !buff->open(IO_WriteOnly) ) return;

  printf("iplayer: Downloading %s\n",xml_url.ascii());

  // flag that we are now processing the download
  processingDownload = true;

  // set the host and perform the HTTP get request
  http->setHost(url.host());
  iRequest = http->get(url.path(),buff);

  // wait for download to complete (but be nice)
  while(processingDownload) { qApp->processEvents(); };

  // if an error occurred during the download, dont process the data
  if (errorFlag) {
    printf("iplayer: QHttp error occurred.\n");
    return;
  }

  // bytes should now be our XML feed, open for reading and pass to the
  //  xml parser.
  QBuffer tmpBuffer( bytes );
  tmpBuffer.open( IO_ReadOnly );
  QTextStream tmpTS( &tmpBuffer );
  QXmlInputSource xmlInputSource( tmpTS );

  QXmlSimpleReader reader;

  // our XMLHandler class will handle the content
  reader.setContentHandler( new XMLHandler() );
  reader.parse( &xmlInputSource ,false);

}

void XMLParser::httpFinished(int reqId, bool error)
{
  // if an error occurred during the download flag it
  //  and flag the end of the download.
  if (error) {
    errorFlag = true;
    processingDownload = false;

  }

  // if this signal was sent for the request we are waiting on
  //  then flag the end of the download and remove the signal handler.
  else if (reqId==iRequest)
  {
    processingDownload = false;
    disconnect( http, SIGNAL(requestFinished(int, bool)), 0, 0 );

  }

}

XMLParser::~XMLParser()
{
}
